1.调参
作者 | 班级 | 学号 |
---|---|---|
唐灏楠 | AI2202 | 0122210880403 |
调参,观察不同参数设置的变化
在tensorflow playground里面,我发现我们主要可以调整以下参数:
在调整神经网络的参数时,一个核心原则是尽可能简化模型,即使用最少的特征和层数来实现高效的分类或者回归。这种方法有助于减少模型的复杂度和计算成本,同时也可以避免过拟合,提高模型的泛化能力,因此,在后续的调参过程中,我将遵循这一原则,力求通过最简单的模型配置达到良好的效果(Test lost < 0.01)。这不仅涉及选择合适数量的隐藏层和节点,还包括精心挑选对模型输出影响最大的特征,以及调整学习率和正则化参数以优化性能。
只用了两个特征,甚至没有使用非线性激活函数以及添加隐藏层,直接就实现了训练集上的完全分类,测试集上的误差也只有0.002,我觉得这个效果已经很好了,因此这个模型训练到这里就行了
模型完美的抓住了数据集的典型特征,实现了训练集上的完美分类,测试集上也只有0.01的误差,loss随着epoch批次收敛,是一个非常好的模型。
模型完美的不行,在训练集和测试集上都没有一点误差出现,甚至没有一层隐藏层。、
验证集和训练集损失都大于0.55, 这还不如我们抛硬币来猜,看来我们必须要加一点隐藏层来捕捉一些有现有特征组成的更高层的特征来辅助我们的判断了。
经过训练后,验证集上的损失达到了0.002,但是验证集上的损失有0.1,说明模型泛化能力不强,还需要改进。
这一次,训练集的损失来到了0.001,而测试集的损失下降到了0.06,虽然理我们的标准(0.01)还有一点距离,但是我们已经可以从第二层的神经元索要识别的特征中找到了类似螺旋的结构了,这说明层数的增加有助于模型学习更复杂的特征结构。
这次的测试集损失更低了,但是还差一点到0.01,同时注意到训练的时间显著变长。
这一次虽然第四层神经元更好的识别到了螺旋的特征,而且测试集更低了,但是权重的剧烈更新简直慢的不行,在大更新前要经历相当长的啥都不变的时期,下次我要增加学习率,让它学的快点,增加步长让他别在一个局部最优解里面度过余生。
这次真的尽力了,Testloss真的不能再低了,使用了RELU函数后训练时间开销显著减少,到了第六层的时候已经学习到了非常明显的螺旋结构,模型也还算可以了。
在本次调参经验中,我深刻感受到了在机器学习项目中特征选择的重要性。良好的特征选择可以极大简化模型的复杂度,使得即使模型结构简单也能取得优异的表现。相反,如果特征选择不当,即便增加更多的层数,模型的表现也可能不尽如人意。此外,我还了解到学习率和激活函数对模型训练速度的影响显著,而且模型层数的增加并不总是带来性能的提升,尤其是考虑到时间成本后,过多的层数可能适得其反。通过这次实践,我对模型调优有了更深入的认识和体会。
由于要随机生成三类每类包含20个样本点的数据,我将随机选取三个三维的坐标,然后以它们作为中心,分别生成三个高斯分布的样本群,每个样本群有20个样本。
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import OneHotEncoder
np.random.seed(50)
# 随机生成三类数据,每类20个样本,数据维度为3, 并设置分别的中心点
class1 = np.random.randn(20, 3) + [1, 1, 1]
class2 = np.random.randn(20, 3) + [4, 2, 1]
class3 = np.random.randn(20, 3) + [5, 2, 9]
X = np.vstack((class1, class2, class3)) # 按行堆叠数据
y = np.array([0]*20 + [1]*20 + [2]*20) # 指定每个样本的类别标签
# 划分训练验证集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=50)
# 独热编码
encoder = OneHotEncoder(sparse=False)
y_train_onehot = encoder.fit_transform(y_train.reshape(-1, 1))
y_test_onehot = encoder.transform(y_test.reshape(-1, 1))
现在我们开始实现最简单的BP神经网络:MLP多层感知机。由于MLP的前馈结构,我们可以将其拆分成许多层,每一层都应该具备3个基本的功能,它们分别是:
激活函数可以单独抽象为单独的一层,不具有参数。
为了形式统一,我们先定义基类,再让不同的层都继承于基类。
# 基类层
class Layer:
# 前向传播函数,根据上一层输入x计算
def forward(self, x):
raise NotImplementedError # 未实现错误
# 反向传播函数,输入下一层回传的梯度grad, 输出当前层的梯度
def backward(self, grad):
raise NotImplementedError
# 更新函数,用于更新当前层的参数
def update(self, learning_rate):
pass
线性层是MLP中最基本的结构之一,其参数为 ( W ) 和 ( b ),输入和输出关系为:
为什么 x
的维度有一个 batch_size
?
在神经网络训练中,通常会将多个输入样本打包成一个批次(batch)进行处理。这种方法被称为批量处理。这样做有以下几个好处:
class Linear(Layer):
def __init__(self, num_in, num_out, use_bias=True):
self.num_in = num_in # 输入维度
self.num_out = num_out # 输出维度
self.use_bias = use_bias # 是否添加偏置
# 参数的初始化(绝对不能初始化为0!不然后续计算失去意义)
# 用正态分布来初始化W
self.W = np.random.normal(loc=0, scale=1.0, size=(num_out, num_in))
if use_bias:
self.b = np.zeros((1, num_out))
def forward(self, x):
# 前向传播 y = Wx + b
# x的维度为(batch_size, num_in)
self.x = x
self.y = x @ self.W.T # y的维度为(batch_size, num_out)
if self.use_bias:
self.y += self.b
return self.y
def backward(self, grad):
# 反向传播,按照链式法则计算
# grad的维度为(batch_size, num_out)
# 梯度应该对batch_size去平均值
# grad_W的维度应该与W相同,为(num_in, num_out)
self.grad_W = self.x.T @ grad / grad.shape[0]
if self.use_bias:
# grad_b的维度与b相同,(1, num_out)
self.grad_b = np.mean(grad, axis=0, keepdims=True) # 对 grad 沿批次维度(行)取平均值,并保留维度信息以确保结果形状与偏置向量 b 一致。
# 往上一层传递的grad维度应该为(batch_size, num_in)
grad = grad @ self.W
return grad
def update(self, learning_rate):
# 更新参数以完成梯度下降
self.W -= learning_rate * self.grad_W
if self.use_bias:
self.b -= learning_rate * self.grad_b
现在我们来实现激活层的设计,以计算反向传播,这里主要实现三种激活层:
class Identity(Layer):
# 啥都不动层
def forward(self, x):
return x
def backward(self, grad):
return grad
class Sigmoid(Layer):
# Sigmoid激活层
def forward(self, x):
self.x = x
self.y = 1 / (1 + np.exp(-x))
return self.y
def backward(self, grad):
return grad * self.y * (1 - self.y)
class Tanh(Layer):
# Tanh激活层
def forward(self, x):
self.x = x
self.y = np.tanh(x)
return self.y
def backward(self, grad):
return grad * (1 - self.y ** 2)
class ReLU(Layer):
# Relu激活层
def forward(self, x):
self.x = x
self.y = np.maximum(x, 0)
return self.y
def backward(self, grad):
return grad * (self.x >= 0)
class Softmax(Layer):
def forward(self, x):
exp_x = np.exp(x - np.max(x, axis=1, keepdims=True))
self.y = exp_x / np.sum(exp_x, axis=1, keepdims=True)
return self.y
def backward(self, grad):
return grad
# 存储所有激活函数和对应名称,方便索引
activation_dict = {
'identity': Identity,
'sigmoid': Sigmoid,
'tanh': Tanh,
'relu': ReLU,
'softmax': Softmax
}
现在我们已经有了用来构建MLP的所有层,现在把它们拼起来即可得到MLP。
class Linear(Layer):
def __init__(self, num_in, num_out, use_bias=True):
self.num_in = num_in # 输入维度
self.num_out = num_out # 输出维度
self.use_bias = use_bias # 是否添加偏置
# 参数的初始化(绝对不能初始化为0!不然后续计算失去意义)
# 用正态分布来初始化W
self.W = np.random.normal(loc=0, scale=1.0, size=(num_in, num_out))
if use_bias:
self.b = np.zeros((1, num_out))
def forward(self, x):
# 前向传播 y = xW + b
# x的维度为(batch_size, num_in)
self.x = x
self.y = x @ self.W # y的维度为(batch_size, num_out)
if self.use_bias:
self.y += self.b
return self.y
def backward(self, grad):
# 反向传播,按照链式法则计算
# grad的维度为(batch_size, num_out)
# 梯度应该对batch_size去平均值
# grad_W的维度应该与W相同,为(num_in, num_out)
self.grad_W = self.x.T @ grad / grad.shape[0]
if self.use_bias:
# grad_b的维度与b相同,(1, num_out)
self.grad_b = np.mean(grad, axis=0, keepdims=True)
# 往上一层传递的grad维度应该为(batch_size, num_in)
grad = grad @ self.W.T
return grad
def update(self, learning_rate):
# 更新参数以完成梯度下降
self.W -= learning_rate * self.grad_W
if self.use_bias:
self.b -= learning_rate * self.grad_b
MLP构建好之后,我们就可以开始进行训练了。由于输入维度为3,输出应该有三个类别
,因此输入和输出神经元都应该有3个。
在机器学习中,随机梯度下降(Stochastic Gradient Descent, SGD)是一种非常常见的优化算法。与标准的梯度下降算法不同,SGD在每次迭代中使用一个或几个随机选取的样本来计算梯度,而不是使用全部训练数据。这样做的好处包括:
下面是具体的训练过程:
import numpy as np
from sklearn.model_selection import train_test_split
import matplotlib.pyplot as plt
# 设置训练参数
num_epochs = 1000
learning_rate = 0.05
batch_size = 20
eps = 1e-7 # 用于防止除0,log(0)等问题
# 创建一个层大小依次为3, 8, 3的多层感知机
# 对于多分类问题,使用softmax作为输出层的激活函数
mlp = MLP(layer_sizes=[3, 8, 3], use_bias=True, activation='relu', out_activation='softmax')
# 记录损失和准确率
train_losses = []
test_losses = []
train_accuracies = []
test_accuracies = []
for epoch in range(num_epochs):
st = 0
loss = 0.0
while st < len(X_train):
ed = min(st + batch_size, len(X_train))
# 取出batch
x_batch = X_train[st:ed]
y_batch = y_train_onehot[st:ed]
# 计算MLP的预测
y_pred = mlp.forward(x_batch)
# 计算损失
batch_loss = -np.sum(np.log(y_pred + eps) * y_batch) / y_batch.shape[0]
loss += batch_loss
# 计算梯度并进行反向传播
grad = y_pred - y_batch
mlp.backward(grad)
# 更新参数
mlp.update(learning_rate)
st = ed
loss /= (len(X_train) / batch_size)
train_losses.append(loss)
# 计算训练准确率
train_acc = np.mean(np.argmax(mlp.forward(X_train), axis=1) == y_train)
train_accuracies.append(train_acc)
# 计算测试损失和准确率
test_loss = -np.sum(np.log(mlp.forward(X_test) + eps) * y_test_onehot) / y_test_onehot.shape[0]
test_losses.append(test_loss)
test_acc = np.mean(np.argmax(mlp.forward(X_test), axis=1) == y_test)
test_accuracies.append(test_acc)
if epoch % 100 == 0:
print(f'Epoch {epoch}, Train Loss: {loss:.4f}, Test Loss: {test_loss:.4f}, Train Acc: {train_acc:.4f}, Test Acc: {test_acc:.4f}')
# 可视化训练和测试的损失与准确率
plt.figure(figsize=(12, 5))
plt.subplot(1, 2, 1)
plt.plot(train_losses, label='Train Loss')
plt.plot(test_losses, label='Test Loss')
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.title('Loss over Epochs')
plt.legend()
plt.subplot(1, 2, 2)
plt.plot(train_accuracies, label='Train Accuracy')
plt.plot(test_accuracies, label='Test Accuracy')
plt.xlabel('Epochs')
plt.ylabel('Accuracy')
plt.title('Accuracy over Epochs')
plt.legend()
plt.show()
总的来说,模型在这次训练中表现非常好,能够快速收敛并保持较高的准确率和较低的损失
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.neural_network import MLPClassifier
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import classification_report, accuracy_score
import matplotlib.pyplot as plt
# 设置随机种子,确保数据生成的可重复性
np.random.seed(42)
# 随机生成三类数据,每类20个样本,数据维度为2
class1 = np.random.randn(20, 2) + [1, 1] # 中心点在(1,1)
class2 = np.random.randn(20, 2) + [5, 5] # 中心点在(5,5)
class3 = np.random.randn(20, 2) + [9, 1] # 中心点在(9,1)
# 汇总数据和标签
X = np.vstack((class1, class2, class3))
y = np.array([0]*20 + [1]*20 + [2]*20) # 分别标记为0, 1, 2
# 划分训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
# 数据标准化
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
# 构建BP神经网络
clf = MLPClassifier(hidden_layer_sizes=(10,), max_iter=1000, alpha=1e-4,
solver='sgd', verbose=10, random_state=1,
learning_rate_init=.1)
# 训练模型
clf.fit(X_train_scaled, y_train)
# 测试集上的预测
y_pred = clf.predict(X_test_scaled)
# 性能评估
print("Classification report for classifier %s:\n%s\n"
% (clf, classification_report(y_test, y_pred)))
print("Accuracy: ", accuracy_score(y_test, y_pred))
# 绘制结果
def plot_decision_boundaries(X, y, model, ax):
h = .02
x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1
y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1
xx, yy = np.meshgrid(np.arange(x_min, x_max, h),
np.arange(y_min, y_max, h))
Z = model.predict(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)
ax.contourf(xx, yy, Z, alpha=0.8)
ax.scatter(X[:, 0], X[:, 1], c=y, edgecolors='k', marker='o', s=50, linewidth=1)
ax.set_xlim(xx.min(), xx.max())
ax.set_ylim(yy.min(), yy.max())
ax.set_xticks(())
ax.set_yticks(())
fig, ax = plt.subplots()
plot_decision_boundaries(X_train_scaled, y_train, clf, ax)
plt.title("Decision surface of neural network")
plt.show()
numpy
的random.randn
函数生成标准正态分布(均值为0,标准差为1)的样本,然后将它们平移到指定的中心位置。[1, 1]
[5, 5]
[9, 1]
这种方式确保每类数据都具有一定的集中趋势,同时又有一些随机性,模拟实际情况中类别数据的分布。
max_iter=1000
,这是训练过程中最大的迭代次数,用以控制训练的持续时间,防止过拟合。learning_rate_init=0.1
,这个参数设定了权重更新的初始步长,较大的学习率可以加快学习进度,但也可能导致训练不稳定。alpha=1e-4
,这是L2正则化系数,用于控制模型的复杂度,避免过拟合。verbose=10
,这个设置使得在训练过程中每10个epoch就输出一次训练状态,便于监控训练进度。codelancera@CodelancerA MINGW64 /e/codelancera/OneDrive/Desktop/DM&ML
$ C:/ProgramData/anaconda3/python.exe "e:/codelancera/OneDrive/Desktop/DM&ML/BP_demo.py"
Iteration 1, loss = 1.49852335
Iteration 2, loss = 1.18474940
Iteration 3, loss = 0.87402893
Iteration 4, loss = 0.63535660
...
Iteration 48, loss = 0.00285642
Iteration 49, loss = 0.00280477
Iteration 50, loss = 0.00275642
Iteration 51, loss = 0.00271143
Training loss did not improve more than tol=0.000100 for 10 consecutive epochs. Stopping.
Classification report for classifier MLPClassifier(hidden_layer_sizes=(10,), learning_rate_init=0.1, max_iter=1000,
random_state=1, solver='sgd', verbose=10):
precision recall f1-score support
0 1.00 1.00 1.00 4
1 1.00 1.00 1.00 2
2 1.00 1.00 1.00 6
accuracy 1.00 12
macro avg 1.00 1.00 1.00 12
weighted avg 1.00 1.00 1.00 12
Accuracy: 1.0